package com.yeskery.nut.scan.bean;

import com.yeskery.nut.annotation.environment.ConfigurationProperties;
import com.yeskery.nut.bean.FactoryBeanRegister;
import com.yeskery.nut.bind.FitValueHelper;
import com.yeskery.nut.core.Environment;
import com.yeskery.nut.core.NutException;
import com.yeskery.nut.scan.BeanAnnotationScanMetadata;
import com.yeskery.nut.scan.BeanCreator;
import com.yeskery.nut.util.ReflectUtils;
import com.yeskery.nut.util.StringUtils;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

/**
 * ConfigurationProperties 注解Bean创建类
 * @author sprout
 * @version 1.0
 * 2022-07-17 21:59
 */
public class ConfigurationPropertiesBeanCreator implements BeanCreator {

    /** 日志对象 */
    private static final Logger logger = Logger.getLogger(ConfigurationPropertiesBeanCreator.class.getName());

    /** 分隔符 */
    private static final char SEPARATOR = '.';

    /** 分隔符字符串 */
    private static final String SEPARATOR_STR = "\\.";

    /** 分组分隔符 */
    private static final String GROUP_SEPARATOR = "-";

    /** 分组开始符 */
    private static final String GROUP_SEPARATOR_START = "[";

    /** 分组结束符 */
    private static final String GROUP_SEPARATOR_END = "]";

    /** Bean注册器 */
    private final FactoryBeanRegister factoryBeanRegister;

    /** 环境对象 */
    private final Environment environment;

    /**
     * 构建ConfigurationProperties 注解Bean创建配置类
     * @param factoryBeanRegister Bean注册器
     * @param environment 环境对象
     */
    public ConfigurationPropertiesBeanCreator(FactoryBeanRegister factoryBeanRegister, Environment environment) {
        this.factoryBeanRegister = factoryBeanRegister;
        this.environment = environment;
    }

    @Override
    public void createBean(BeanAnnotationScanMetadata beanAnnotationScanMetadata) {
        if (beanAnnotationScanMetadata.getSource() != BeanAnnotationScanMetadata.Source.CONFIGURATION_PROPERTIES) {
            return;
        }

        FitValueHelper fitValueHelper = FitValueHelper.getInstance();
        Class<?> clazz = beanAnnotationScanMetadata.getType();

        Object instance;
        try {
            Constructor<?> constructor = clazz.getConstructor();
            instance = constructor.newInstance();
        } catch (NoSuchMethodException e) {
            throw new NutException("@ConfigurationProperties Class [" + clazz.getName() + "] Need No Parameter Constructor.", e);
        } catch (Exception e) {
            throw new NutException("@ConfigurationProperties Class [" + clazz.getName() + "] Instance Create Fail.", e);
        }

        ConfigurationProperties configurationProperties = clazz.getAnnotation(ConfigurationProperties.class);
        String prefix = "";
        if (!StringUtils.isEmpty(configurationProperties.prefix())) {
            prefix = configurationProperties.prefix();
        } else if (!StringUtils.isEmpty(configurationProperties.value())) {
            prefix = configurationProperties.value();
        }
        prefix = prefix.trim();
        for (Map.Entry<String, String> entry : getEnvPrefixAttributes(prefix).entrySet()) {
            String[] splits = entry.getKey().replace(prefix + SEPARATOR, "").split(SEPARATOR_STR);
            Object target = instance;
            if (splits.length <= 1) {
                bindConfigurationProperty(fitValueHelper, target, splits[0], entry.getValue());
            } else {
                for (int i = 0; i < splits.length; i++) {
                    if (i == splits.length - 1) {
                        bindConfigurationProperty(fitValueHelper, target, splits[i], entry.getValue());
                    } else {
                        Field field = null;
                        try {
                            field = target.getClass().getDeclaredField(splits[i]);
                            Object object = ReflectUtils.getObjectFieldValue(target, field);
                            if (object == null) {
                                try {
                                    Constructor<?> constructor = field.getType().getConstructor();
                                    object = constructor.newInstance();
                                    ReflectUtils.setObjectFieldValue(target, field, object);
                                } catch (NoSuchMethodException e) {
                                    throw new NutException("@ConfigurationProperties Field Class ["
                                            + field.getType().getName() + "] Need No Parameter Constructor.", e);
                                } catch (Exception e) {
                                    throw new NutException("@ConfigurationProperties Field Class ["
                                            + field.getType().getName() + "] Instance Create Fail.", e);
                                }
                            }

                            target = object;
                        } catch (NoSuchFieldException e) {
                            logger.logp(Level.SEVERE, this.getClass().getName(), "handle",
                                    "Cannot Find @ConfigurationProperties Field [" + splits[i] +"]", e);
                            break;
                        } catch (Exception e) {
                            if (field == null) {
                                throw new NutException("@ConfigurationProperties Field Bind Fail.", e);
                            }
                            throw new NutException("@ConfigurationProperties Field Name[" + field.getName()
                                    + "] Class [" + field.getType().getName() + "] Bind Fail.", e);
                        }
                    }
                }
            }

        }
        factoryBeanRegister.registerBean(ReflectUtils.getDefaultBeanName(clazz), instance);
    }

    /**
     * 获取指定前缀的环境属性
     * @param prefix 前缀
     * @return 指定前缀的环境属性
     */
    private SortedMap<String, String> getEnvPrefixAttributes(String prefix) {
        return environment.getEnvProperties().entrySet()
                .stream()
                .filter(e -> e != null && e.getKey() != null && e.getValue() != null)
                .filter(e -> StringUtils.isEmpty(prefix) || e.getKey().toString().startsWith(prefix))
                .collect(Collectors.toMap(e -> e.getKey().toString(), e -> e.getValue().toString(),
                        (u, v) -> {throw new IllegalStateException(String.format("Duplicate key %s", u));},
                        () -> new TreeMap<>(Comparator.comparingInt(this::getKeySeparatorSize).thenComparing(String::compareTo))));
    }

    /**
     * 获取key的分隔符数量
     * @param key key
     * @return key的分隔符数量
     */
    private int getKeySeparatorSize(String key) {
        return StringUtils.getKeySeparatorSize(key, SEPARATOR_STR);
    }

    /**
     * 绑定配置属性
     * @param fitValueHelper 合适值帮助类
     * @param target 配置对象
     * @param name 配置名称
     * @param value 配置值
     */
    @SuppressWarnings("unchecked")
    private void bindConfigurationProperty(FitValueHelper fitValueHelper, Object target, String name, String value) {
        Field field = null;
        try {
            int startIndex = name.indexOf(GROUP_SEPARATOR_START);
            int endIndex = name.lastIndexOf(GROUP_SEPARATOR_END);
            String express = "";
            if (startIndex != -1 && endIndex != -1 && startIndex < endIndex) {
                express = name.substring(startIndex + 1, endIndex);
                name = name.substring(0, startIndex);
            }

            field = target.getClass().getDeclaredField(name);
            if (fitValueHelper.isBasicTypeClass(field.getType())) {
                ReflectUtils.setObjectFieldValue(target, field, fitValueHelper.getFitParamValue(field.getName(), value, field.getType()).getData());
            } else {
                if (field.getType().isArray()) {
                    Class<?> arrayComponentType = field.getType().getComponentType();
                    if (!fitValueHelper.isBasicTypeClass(arrayComponentType)) {
                        throw new NutException("@ConfigurationProperties Field [" + field.getName() + "] Only Support Base Type.");
                    }
                    if (StringUtils.isEmpty(express)) {
                        String[] splits = value.split(",");
                        Object array = Array.newInstance(arrayComponentType, splits.length);
                        for (int i = 0; i < splits.length; i++) {
                            Array.set(array, i, fitValueHelper.getFitParamValue(field.getName(), splits[i].trim(), arrayComponentType).getData());
                        }
                        ReflectUtils.setObjectFieldValue(target, field, array);
                    } else {
                        int index = Integer.parseInt(express);
                        if (index < 0) {
                            throw new NutException("@ConfigurationProperties Field [" + field.getName() + "] Array Index [" + index + "] Invalid.");
                        }
                        Object fileValue = ReflectUtils.getObjectFieldValue(target, field);
                        if (fileValue == null) {
                            Object array = Array.newInstance(arrayComponentType, index + 1);
                            Array.set(array, index, fitValueHelper.getFitParamValue(field.getName(), value, arrayComponentType).getData());
                            ReflectUtils.setObjectFieldValue(target, field, array);
                        } else {
                            int length = Array.getLength(fileValue);
                            if (index >= length) {
                                Object array = Array.newInstance(arrayComponentType, index + 1);
                                System.arraycopy(fileValue, 0, array, 0, length);
                                Array.set(array, index, fitValueHelper.getFitParamValue(field.getName(), value, arrayComponentType).getData());
                                ReflectUtils.setObjectFieldValue(target, field, array);
                            }
                        }
                    }
                } else if (Collection.class.isAssignableFrom(field.getType())) {
                    Class<?> clazz = ReflectUtils.getActualTypeArgumentType(field.getGenericType(), 0);
                    if (!fitValueHelper.isBasicTypeClass(clazz)) {
                        throw new NutException("@ConfigurationProperties Field [" + field.getName() + "] Only Support Base Type.");
                    }
                    if (StringUtils.isEmpty(express)) {
                        String[] splits = value.split(",");
                        Collection<Object> collection = ReflectUtils.createFieldTypeCollection(field, splits.length);
                        for (String split : splits) {
                            collection.add(fitValueHelper.getFitParamValue(field.getName(), split.trim(), clazz).getData());
                        }
                        ReflectUtils.setObjectFieldValue(target, field, collection);
                    } else {
                        Collection<Object> collection;
                        Object fileValue = ReflectUtils.getObjectFieldValue(target, field);
                        if (fileValue == null) {
                            collection = ReflectUtils.createFieldTypeCollection(field, null);
                            ReflectUtils.setObjectFieldValue(target, field, collection);
                        } else {
                            collection = (Collection<Object>) fileValue;
                        }
                        collection.add(fitValueHelper.getFitParamValue(field.getName(), value, clazz).getData());
                    }
                } else if (Map.class.isAssignableFrom(field.getType())) {
                    Class<?> keyClazz = ReflectUtils.getActualTypeArgumentType(field.getGenericType(), 0);
                    if (!String.class.equals(keyClazz)) {
                        throw new NutException("@ConfigurationProperties Field [" + field.getName() + "] Only Support Map Key String Type.");
                    }
                    Class<?> valueClazz = ReflectUtils.getActualTypeArgumentType(field.getGenericType(), 1);
                    if (!fitValueHelper.isBasicTypeClass(valueClazz)) {
                        throw new NutException("@ConfigurationProperties Field [" + field.getName() + "] Only Support Map Value Base Type.");
                    }
                    if (StringUtils.isEmpty(express)) {
                        String[] splits = value.split(",");
                        Map<String, Object> map = new HashMap<>(splits.length);
                        for (String split : splits) {
                            String[] keyValue = split.trim().split("=");
                            if (keyValue.length != 2) {
                                throw new NutException("@ConfigurationProperties Field [" + field.getName() + "] Map Key Value Format Invalid.");
                            }
                            map.put(keyValue[0], fitValueHelper.getFitParamValue(field.getName(), keyValue[1], valueClazz).getData());
                        }
                        ReflectUtils.setObjectFieldValue(target, field, map);
                    } else {
                        Object fileValue = ReflectUtils.getObjectFieldValue(target, field);
                        Map<String, Object> map;
                        if (fileValue == null) {
                            map = new HashMap<>();
                            ReflectUtils.setObjectFieldValue(target, field, map);
                        } else {
                            map = (Map<String, Object>) fileValue;
                        }
                        map.put(express, fitValueHelper.getFitParamValue(field.getName(), value, valueClazz).getData());
                    }
                } else {
                    throw new NutException("UnSupport @ConfigurationProperties Field Class [" + field.getType().getName() + "]");
                }
            }
        } catch (NoSuchFieldException e) {
            String modifyName;
            if (name.contains(GROUP_SEPARATOR) && !StringUtils.isEmpty(modifyName = StringUtils.getModifyName(name)) && !modifyName.equals(name)) {
                bindConfigurationProperty(fitValueHelper, target, modifyName, value);
            }
        } catch (Exception e) {
            if (field == null) {
                throw new NutException("@ConfigurationProperties Field Bind Fail.", e);
            }
            throw new NutException("@ConfigurationProperties Field Name[" + field.getName()
                    + "] Class [" + field.getType().getName() + "] Bind Fail.", e);
        }
    }
}
